<!--
 * @Author: yanjikun
 * @Date: 2022-01-05 16:32:02
 * @LastEditors: yanjikun
 * @LastEditTime: 2022-05-17 11:06:38
 * @Description: file content
-->
<template>
  <Col v-bind="config.colProps" v-if="!getHidden">
    <Divider v-if="config.component === 'Divider'" v-bind="getComponentProps">
      <component :is="renderLabelHelpMessage"></component>
    </Divider>
    <Form.Item
      :name="config.field"
      :label="renderLabelHelpMessage"
      :required="config.required"
      v-bind="config.formItemProps"
      :label-col="itemLabelWidthProp.labelCol"
      :wrapper-col="itemLabelWidthProp.wrapperCol"
      :rules="getRules"
      v-else
    >
      <slot v-if="config.slot" :name="config.slot" v-bind="getValues"></slot>
      <component
        :is="getComponent"
        v-else-if="getComponent"
        style="width: 100%"
        v-bind="getComponentProps"
        v-on="getComponentEvents"
        v-model:[modelValueType]="modelValue[config.field]"
        :key="config.field"
        :allow-clear="true"
        :disabled="getDisable"
      >
        <template
          v-for="(slotFn, slotName) in getComponentSlots"
          #[slotName]="slotData"
          :key="slotName"
        >
          <component :is="slotFn?.({ ...getValues, slotData })" :key="slotName"></component>
        </template>
      </component>
    </Form.Item>
  </Col>
</template>

<script lang="tsx" setup>
  import { Col, Form, Divider } from 'ant-design-vue'
  import { componentMap } from '../componentMap'
  import { BasicProps } from '../props'
  import { CustomRenderFn, FormConfig, RenderCallbackParams } from '../types/form'
  import { useItemLabelWidth } from '../hooks/useLabelWidth'
  import { isArray, isFunction, isString } from '@/utils/is'
  import { useVModel } from '@vueuse/core'
  import { cloneDeep, isBoolean, isNull, isObject } from 'lodash'
  import { computed, isVNode, PropType, toRefs, unref } from 'vue'
  import { RuleObject } from 'ant-design-vue/es/form'
  import { useFormContext } from '../hooks/useFormContext'
  import BasicHelp from '@/components/BasicHelp'
  defineOptions({
    name: 'BasicFormItem',
  })
  const props = defineProps({
    config: {
      type: Object as PropType<FormConfig>,
      default: () => ({}),
    },
    formProps: {
      type: Object as PropType<BasicProps>,
      default: () => ({}),
    },
    formModel: {
      type: Object as PropType<Recordable>,
      default: () => ({}),
    },
    setFormModel: {
      type: Function as PropType<(key: string, value: any) => void>,
      default: null,
    },
  })

  const formContext = useFormContext()
  const emit = defineEmits(['update:formModel'])
  /**获取表单值 */
  const getValues = computed<RenderCallbackParams>(() => {
    const { formModel, config } = props
    return {
      field: config.field,
      formInstance: formContext,
      formModel,
      config,
      values: {
        ...formModel,
      } as Recordable,
    }
  })

  const getHidden = computed(() => {
    const { hidden } = props.config
    if (isBoolean(hidden)) {
      return hidden
    }
    if (isFunction(hidden)) {
      return hidden(unref(getValues))
    }
    return !!hidden
  })
  /**表单项禁用 */
  const getDisable = computed(() => {
    const { dynamicDisabled } = props.config
    const { disabled: itemDisabled = false } = unref(getComponentProps)
    let disabled = itemDisabled
    if (isBoolean(dynamicDisabled)) {
      disabled = dynamicDisabled
    }
    if (isFunction(dynamicDisabled)) {
      disabled = dynamicDisabled(unref(getValues))
    }
    return disabled
  })
  /** 生成vnode */
  const vnodeFactory = (
    component: FormConfig['componentSlots'] | FormConfig['component'],
    values = unref(getValues),
  ) => {
    if (isString(component)) {
      return <>{component}</>
    } else if (isVNode(component)) {
      return component
    } else if (isFunction(component)) {
      return vnodeFactory((component as CustomRenderFn)(values))
    } else if (isObject(component)) {
      const comKeys = Object.keys(component)
      if (comKeys.some((n) => n.startsWith('_') || ['setup', 'render'].includes(n))) {
        return component
      }
      return comKeys.reduce<Recordable<CustomRenderFn>>((slots, slotName) => {
        slots[slotName] = (...rest: any) => vnodeFactory(component[slotName], rest)
        return slots
      }, {})
    }
    return component
  }
  /**当前表单项组件 */
  const getComponent = computed(() => {
    const component = props.config.component
    return isString(component)
      ? componentMap.get(component) ?? vnodeFactory(component)
      : vnodeFactory(component)
  })

  const getComponentSlots = computed<Recordable<CustomRenderFn>>(() => {
    const componentSlots = props.config.componentSlots ?? {}
    return isString(componentSlots) || isVNode(componentSlots)
      ? {
          default: (...rest: any) => vnodeFactory(componentSlots, rest),
        }
      : vnodeFactory(componentSlots)
  })
  /**表单组件Props */
  const getComponentProps = computed(() => {
    const { config } = props
    let { componentProps = {}, component } = config
    if (isFunction(componentProps)) {
      componentProps = componentProps(unref(getValues)) ?? {}
    }
    if (component === 'Divider') {
      componentProps = Object.assign({ type: 'horizontal' }, componentProps, {
        orientation: 'left',
        plain: true,
      })
    }
    return componentProps as Recordable
  })
  /**表单组件Events */
  const getComponentEvents = computed(() => {
    const componentProps = (props.config.componentProps ?? {}) as Recordable
    return Object.keys(componentProps).reduce((prev, key) => {
      if (/on[A-Z]/.test(key)) {
        const eventKey = key.replace(/on([A-Z])/, '$1').toLocaleLowerCase()
        prev[eventKey] = componentProps[key]
      }
      return prev
    }, {} as Recordable)
  })

  const renderLabelHelpMessage = computed(() => {
    const { label, helpMessage, helpComponentProps } = props.config
    const renderLabel = vnodeFactory(label)
    const getHelpMessage = isFunction(helpMessage) ? helpMessage(unref(getValues)) : helpMessage
    if (!getHelpMessage || (isArray(getHelpMessage) && getHelpMessage.length === 0)) {
      return renderLabel
    }
    return (
      <span>
        {renderLabel}
        <BasicHelp class="mx-1" text={getHelpMessage} {...helpComponentProps}></BasicHelp>
      </span>
    )
  })
  const getRules = computed(() => {
    const {
      rules: defaultRules = [],
      required,
      message: defaultMessage = '请填写数据',
    } = props.config
    let rules: RuleObject[] = cloneDeep(defaultRules) as RuleObject[]
    const validator = (rule: any, value: any) => {
      const msg = rule.msg || defaultMessage
      if (value === undefined || isNull(value)) {
        return Promise.reject(msg)
      } else if (isArray(value) && value.length === 0) {
        return Promise.reject(msg)
      } else if (isString(value) && value.trim() === '') {
        return Promise.reject(msg)
      } else if (
        typeof value === 'object' &&
        Reflect.has(value, 'checked') &&
        Reflect.has(value, 'halfChecked') &&
        Array.isArray(value.checked) &&
        Array.isArray(value.halfChecked) &&
        value.checked.length === 0 &&
        value.halfChecked.length === 0
      ) {
        // 非关联选择的tree组件
        return Promise.reject(msg)
      }
      return Promise.resolve()
    }
    if ((!rules || rules.length === 0) && required) {
      rules = [{ required, validator }]
    }
    return rules
  })
  const modelValueType = computed(() => {
    const { component } = config.value
    const isCheck = isString(component) && ['Switch', 'Checkbox'].includes(component)
    return isCheck ? 'checked' : 'value'
  })
  const modelValue = useVModel(props, 'formModel', emit)
  const { config, formProps } = toRefs(props)
  const itemLabelWidthProp = useItemLabelWidth(config, formProps)
</script>

<style scoped></style>
